package expertise.titan.dao.vertex;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

import javax.annotation.Resource;

import org.apache.log4j.Logger;

import com.thinkaurelius.titan.core.TitanTransaction;
import com.thinkaurelius.titan.diskstorage.StorageException;
import com.tinkerpop.blueprints.Vertex;

import expertise.titan.annotation.VertexKey;
import expertise.titan.dao.exception.DaoConfigurationException;
import expertise.titan.environment.ExpertiseTitanEnvironmentConfigurer;
import expertise.titan.schema.vertex.AbstractVertexSchema;

/**
 * This is the abstract DAO for verticies. This will allow common vertex DAO
 * functionality to be modularized in a single place.
 * 
 * @author bdgould
 * 
 * @param <T>
 *            The type of Vertex being accessed
 */
public abstract class AbstractVertexDaoImpl<T extends AbstractVertexSchema> implements VertexDao<T> {

    private static final Logger log = Logger.getLogger(AbstractVertexDaoImpl.class);

    private final Field[] targetFields = new Field[2];

    protected final Map<String, Field> fieldMap = new HashMap<String, Field>();

    protected final Field nameField;

    protected final Field typeField;

    protected final String vertexType;

    protected Class<T> schemaType;

    @Resource
    protected ExpertiseTitanEnvironmentConfigurer titanEnvironment;

    public AbstractVertexDaoImpl(final String vertexType, final Class<T> clazz) throws DaoConfigurationException {
        populateFieldMap(clazz);
        this.vertexType = vertexType;
        this.schemaType = clazz;
        this.nameField = targetFields[0];
        this.typeField = targetFields[1];
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void create(final TitanTransaction transaction, final T instance) throws StorageException {
        final Vertex v = transaction.addVertex(null);
        configureVertexWithSchema(v, instance);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public T read(final TitanTransaction transaction, final String name) throws StorageException {
        final Vertex v = transaction.getVertex(titanEnvironment.vertexNameKey(), name);
        return createSchemaFromVertex(v);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void update(final TitanTransaction transaction, final T updatedInstance, final String name) throws StorageException {
        final Vertex v = transaction.getVertex(titanEnvironment.vertexNameKey(), name);
        configureVertexWithSchema(v, updatedInstance);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void delete(final TitanTransaction transaction, final T instance) throws StorageException {
        final Vertex v = transaction.getVertex(titanEnvironment.vertexNameKey(), instance.getName());
        transaction.removeVertex(v);
    }

    public T createSchemaFromVertex(final Vertex v) {
        if (v != null) {
            final T obj = newSchemaInstance();
            if (obj != null) {
                for (String vertexProperty : v.getPropertyKeys()) {
                    if (fieldMap.containsKey(vertexProperty)) {
                        try {
                            Field f = fieldMap.get(vertexProperty);
                            f.setAccessible(true);
                            f.set(obj, v.getProperty(vertexProperty));
                        } catch (IllegalArgumentException | IllegalAccessException e) {
                            // TODO Auto-generated catch block
                            log.error("", e);
                        }
                    } else {
                        log.warn("No mapped field from vertex property: " + vertexProperty + " in schema object: " + schemaType.getName());
                    }
                }
            }
            return obj;
        } else {
            return null;
        }
    }

    protected T newSchemaInstance() {
        try {
            return schemaType.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            // TODO Auto-generated catch block
            log.error("Issue instantiating new instance of " + schemaType.getName(), e);
            return null;
        }
    }

    protected void configureVertexWithSchema(final Vertex v, final T instance) {
        for (final String key : fieldMap.keySet()) {
            try {
                Field f = fieldMap.get(key);
                f.setAccessible(true);
                v.setProperty(key, f.get(instance));
            } catch (IllegalArgumentException | IllegalAccessException e) {
                // TODO Auto-generated catch block
                log.error("", e);
            }
        }
    }

    protected Vertex[] fromIterable(final Iterable<Vertex> vs, final int max) {
        assert max > 0;
        Vertex[] arr = new Vertex[max];
        int i = 0;
        for (Vertex v : vs) {
            arr[i++] = v;
            if (i >= max)
                break;
        }
        return arr;
    }

    /**
     * Parses the class for the {@link VertexKey} fieldMap.
     * 
     * @param clazz
     *            The schema object class
     * @throws DaoConfigurationException
     *             If there is an issue parsing the class
     */
    private void populateFieldMap(final Class<?> clazz) throws DaoConfigurationException {
        Class<?> tmp = clazz;
        while (tmp != null && !tmp.equals(Object.class)) {
            for (final Field f : tmp.getDeclaredFields()) {
                final VertexKey p = f.getAnnotation(VertexKey.class);
                if (p != null) {
                    if (p.name()) {
                        if (targetFields[0] != null) {
                            throw new DaoConfigurationException("Schema object " + clazz.getName() + " can have only one name Vertex property");
                        } else {
                            targetFields[0] = f;
                        }
                    } else if (p.type()) {
                        if (targetFields[1] != null) {
                            throw new DaoConfigurationException("Schema object " + clazz.getName() + " can have only one type Vertex property");
                        } else {
                            targetFields[1] = f;
                        }
                    }
                    if (fieldMap.containsKey(p.value())) {
                        log.warn("Multiple vertex property keys '" + p.value() + "' in class " + clazz.getName());
                    }
                    fieldMap.put(p.value(), f);
                }
            }
            tmp = tmp.getSuperclass();
        }
    }
}
